/**
 * Copyright (C) 2010-2016 eBusiness Information, Excilys Group
 * Copyright (C) 2016-2020 the ohosannotations project
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed To in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package org.ohosannotations.helper;

import com.helger.jcodemodel.JFieldRef;

import org.ohosannotations.OhosAnnotationsEnvironment;
import org.ohosannotations.annotations.EditorAction;
import org.ohosannotations.annotations.OnAbilityResult;
import org.ohosannotations.annotations.ResId;
import org.ohosannotations.internal.rclass.Rclass;
import org.ohosannotations.logger.Logger;
import org.ohosannotations.logger.LoggerFactory;
import org.ohosannotations.rclass.IRClass;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.NestingKind;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.MirroredTypeException;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;

import static org.ohosannotations.helper.ModelConstants.VALID_ENHANCED_COMPONENT_ANNOTATIONS;
import static org.ohosannotations.helper.ModelConstants.classSuffix;

/**
 * 注释的助手
 *
 * @author dev
 * @since 2021-07-22
 */
public class AnnotationHelper {
    /**
     * 默认字段名称值
     */
    public static final String DEFAULT_FIELD_NAME_VALUE = "value";
    /**
     * 默认字段名resname
     */
    public static final String DEFAULT_FIELD_NAME_RESNAME = "resName";

    private static final Logger LOGGER = LoggerFactory.getLogger(AnnotationHelper.class);

    private final OhosAnnotationsEnvironment environment;

    /**
     * 注释的助手
     *
     * @param environment 环境
     */
    public AnnotationHelper(OhosAnnotationsEnvironment environment) {
        this.environment = environment;
    }

    /**
     * 让环境
     *
     * @return {@link OhosAnnotationsEnvironment}
     */
    public OhosAnnotationsEnvironment getEnvironment() {
        return environment;
    }

    /**
     * getProcessingEnvironment
     *
     * @return environment
     */
    public ProcessingEnvironment getProcessingEnvironment() {
        return environment.getProcessingEnvironment();
    }

    /**
     * Tests whether one type is a subtype of another. Any type is considered to be
     * a subtype of itself.
     *
     * @param potentialSubtype potentialSubtype
     * @param potentialSupertype potentialSupertype
     * @return boolean
     */
    public boolean isSubtype(TypeMirror potentialSubtype, TypeMirror potentialSupertype) {
        return getTypeUtils().isSubtype(potentialSubtype, potentialSupertype);
    }

    /**
     * isSubtype
     *
     * @param t1 t1
     * @param t2 t2
     * @return isSubtype
     */
    public boolean isSubtype(TypeElement t1, TypeElement t2) {
        return isSubtype(t1.asType(), t2.asType());
    }

    /**
     * directSupertypes
     *
     * @param typeMirror typeMirror
     * @return getTypeUtils
     */
    public List<? extends TypeMirror> directSupertypes(TypeMirror typeMirror) {
        return getTypeUtils().directSupertypes(typeMirror);
    }

    /**
     * This method may return null if the {@link TypeElement} cannot be found in the
     * processor classpath
     *
     * @param qualifiedName qualifiedName
     * @return TypeElement
     */
    public TypeElement typeElementFromQualifiedName(String qualifiedName) {
        return getElementUtils().getTypeElement(qualifiedName);
    }

    /**
     * isAnnotatedWith
     *
     * @param element element
     * @param qualifiedName qualifiedName
     * @return false
     */
    public boolean isAnnotatedWith(Element element, String qualifiedName) {
        for (AnnotationMirror annotationMirror : element.getAnnotationMirrors()) {
            if (annotationMirror.getAnnotationType().toString().equals(qualifiedName)) {
                return true;
            }
        }
        return false;
    }

    /**
     * QualifiedName
     *
     * @param qualifiedName qualifiedName
     * @return qualifiedName
     */
    public String generatedClassQualifiedNameFromQualifiedName(String qualifiedName) {
        TypeElement type = typeElementFromQualifiedName(qualifiedName);
        if (type.getNestingKind() == NestingKind.MEMBER) {
            String parentGeneratedClass
                = generatedClassQualifiedNameFromQualifiedName(type.getEnclosingElement().asType().toString());
            return parentGeneratedClass + "."
                + type.getSimpleName().toString() + classSuffix();
        } else {
            return qualifiedName + classSuffix();
        }
    }

    /**
     * findAnnotationMirror
     *
     * @param annotatedElement annotatedElement
     * @param annotationName annotationName
     * @return null
     */
    public AnnotationMirror findAnnotationMirror
    (Element annotatedElement, String annotationName) {
        List<? extends AnnotationMirror> annotationMirrors
            = annotatedElement.getAnnotationMirrors();

        for (AnnotationMirror annotationMirror : annotationMirrors) {
            TypeElement annotationElement
                = (TypeElement) annotationMirror.getAnnotationType().asElement();
            if (isAnnotation(annotationElement, annotationName)) {
                return annotationMirror;
            }
        }
        return null;
    }

    /**
     * isAnnotation
     *
     * @param annotation annotation
     * @param annotationName annotationName
     * @return annotation
     */
    public boolean isAnnotation(TypeElement annotation, String annotationName) {
        return annotation.getQualifiedName().toString().equals(annotationName);
    }

    /**
     * isPrivate
     *
     * @param element element
     * @return element
     */
    public boolean isPrivate(Element element) {
        return element.getModifiers().contains(Modifier.PRIVATE);
    }

    /**
     * isPublic
     *
     * @param element element
     * @return element
     */
    public boolean isPublic(Element element) {
        return element.getModifiers().contains(Modifier.PUBLIC);
    }

    /**
     * isStatic
     *
     * @param element element
     * @return element
     */
    public boolean isStatic(Element element) {
        return element.getModifiers().contains(Modifier.STATIC);
    }

    /**
     * isAbstract
     *
     * @param element element
     * @return element
     */
    public boolean isAbstract(Element element) {
        return element.getModifiers().contains(Modifier.ABSTRACT);
    }

    /**
     * isInterface
     *
     * @param element element
     * @return element
     */
    public boolean isInterface(TypeElement element) {
        return element.getKind().isInterface();
    }

    /**
     * isTopLevel
     *
     * @param element element
     * @return isTopLevel
     */
    public boolean isTopLevel(TypeElement element) {
        return element.getNestingKind() == NestingKind.TOP_LEVEL;
    }

    /**
     * isFinal
     *
     * @param element element
     * @return element
     */
    public boolean isFinal(Element element) {
        return element.getModifiers().contains(Modifier.FINAL);
    }

    /**
     * isSynchronized
     *
     * @param element element
     * @return element
     */
    public boolean isSynchronized(Element element) {
        return element.getModifiers().contains(Modifier.SYNCHRONIZED);
    }

    /**
     * getElementUtils
     *
     * @return elements
     */
    public Elements getElementUtils() {
        Elements elements = getProcessingEnvironment().getElementUtils();
        return elements;
    }

    /**
     * getTypeUtils
     *
     * @return getProcessingEnvironment
     */
    public Types getTypeUtils() {
        return getProcessingEnvironment().getTypeUtils();
    }

    /**
     * Returns a list of {@link JFieldRef} linking to the R class, based on the
     * given annotation
     *
     * @param element element
     * @param annotationName annotationName
     * @param rClass rClass
     * @param res res
     * @param useElementName useElementName
     * @return List<>
     * @see #extractAnnotationResources(Element, String, IRClass, IRClass.Res, boolean)
     */
    public List<JFieldRef> extractAnnotationFieldRefs(Element element,
        String annotationName, IRClass rClass, IRClass.Res res, boolean useElementName) {
        return extractAnnotationFieldRefs(element, annotationName,
            rClass, res, useElementName, DEFAULT_FIELD_NAME_VALUE, DEFAULT_FIELD_NAME_RESNAME);
    }

    /**
     * extractAnnotationFieldRefs
     *
     * @param element element
     * @param annotationName annotationName
     * @param rClass rClass
     * @param res res
     * @param useElementName useElementName
     * @param idFieldName idFieldName
     * @param resFieldName resFieldName
     * @return fieldRefs
     */
    public List<JFieldRef> extractAnnotationFieldRefs(Element element,
        String annotationName, IRClass rClass, IRClass.Res res,
        boolean useElementName, String idFieldName, String resFieldName) {
        List<JFieldRef> fieldRefs = new ArrayList<>();

        for (String refQualifiedName : extractAnnotationResources(element,
            annotationName, rClass, res, useElementName, idFieldName, resFieldName)) {
            fieldRefs.add(Rclass.extractIdStaticRef(environment, refQualifiedName));
        }

        return fieldRefs;
    }

    /**
     * Method to handle all annotations dealing with resource ids that can be set
     * using the value() parameter of the annotation (as int or int[]), the
     * resName() parameter of the annotation (as String or String[]), the element
     * name.
     *
     * @param element the annotated element
     * @param annotationName the annotation on the element
     * @param rClass the R innerClass the resources belong to
     * @param res res
     * @param useElementName Should we use a default fallback strategy that uses the element
     * qualified name for a resource name
     * @return the qualified names of the matching resources in the R inner class
     */
    public List<String> extractAnnotationResources(Element element,
        String annotationName, IRClass rClass, IRClass.Res res, boolean useElementName) {
        return extractAnnotationResources(element, annotationName,
            rClass, res, useElementName, DEFAULT_FIELD_NAME_VALUE, DEFAULT_FIELD_NAME_RESNAME);
    }

    /**
     * extractAnnotationResources
     *
     * @param element element
     * @param annotationName annotationName
     * @param rClass rClass
     * @param res res
     * @param useElementName useElementName
     * @param idFieldName idFieldName
     * @param resFieldName resFieldName
     * @return resourceIdQualifiedNames
     */
    public List<String> extractAnnotationResources(Element element,
        String annotationName, IRClass rClass, IRClass.Res res,
        boolean useElementName, String idFieldName, String resFieldName) {
        int[] values = extractAnnotationResIdValueParameter(element, annotationName, idFieldName);

        List<String> resourceIdQualifiedNames = new ArrayList<>();
        /*
         * if nothing defined in the annotation value() parameter, we check for its
         * resName() parameter
         */
        if (defaultResIdValue(values)) {
            String[] resNames = extractAnnotationResNameParameter(element, annotationName, resFieldName);

            if (defaultResName(resNames)) {
                /*
                 * if we mustn't use the element name, then we'll return an empty list
                 */
                if (useElementName) {
                    /*
                     * fallback, using element name
                     */
                    String elementName = extractElementName(element, annotationName);
                    String elementNewName = reName(elementName, res);

                    String clickQualifiedId = rClass.getIdQualifiedNameByName(elementNewName);
                    if (clickQualifiedId == null) {
                        String elementName2 = extractElementName(element, annotationName);
                        String elementNewName2 = RnameTool.humpToLine2(elementName2);
                        elementNewName2 = reName(elementNewName2, res);
                        clickQualifiedId = rClass.getIdQualifiedNameByName(elementNewName2);
                    }
                    resourceIdQualifiedNames.add(clickQualifiedId);
                }
            } else {
                /*
                 * The result will will contain all the resource qualified names based on the
                 * resource names in the resName() parameter
                 */
                for (String resName : resNames) {
                    String elementNewName = reName(resName, res);
                    String resourceIdQualifiedName = rClass.getIdQualifiedNameByName(elementNewName);
                    resourceIdQualifiedNames.add(resourceIdQualifiedName);
                }
            }

        } else {
            /*
             * The result will will contain all the resource qualified names based on the
             * integers in the value() parameter
             */
            for (int value : values) {
                String resourceIdQualifiedName = rClass.getIdQualifiedNameById(value);
                resourceIdQualifiedNames.add(resourceIdQualifiedName);
            }
        }
        return resourceIdQualifiedNames;
    }

    /**
     * reName
     *
     * @param elementName elementName
     * @param res res
     * @return res
     */
    private String reName(String elementName, IRClass.Res res) {
        return res.rName() + "_" + elementName;
    }

    /**
     * extractElementName
     *
     * @param element element
     * @param annotationName annotationName
     * @return elementName
     */
    public String extractElementName(Element element, String annotationName) {
        String elementName = element.getSimpleName().toString();
        int lastIndex = elementName.lastIndexOf(actionName(annotationName));
        if (lastIndex != -1) {
            elementName = elementName.substring(0, lastIndex);
        }
        return elementName;
    }

    /**
     * defaultResName
     *
     * @param resNames resNames
     * @return resNames
     */
    public boolean defaultResName(String[] resNames) {
        return resNames.length == 0 || resNames.length == 1 && "".equals(resNames[0]);
    }

    /**
     * defaultResIdValue
     *
     * @param values values
     * @return values
     */
    public boolean defaultResIdValue(int[] values) {
        return values.length == 0 || values.length == 1 && values[0] == ResId.DEFAULT_VALUE;
    }

    /**
     * ResNameParameter
     *
     * @param element element
     * @param annotationName annotationName
     * @return extractAnnotationResNameParameter
     */
    public String[] extractAnnotationResNameParameter(Element element, String annotationName) {
        return extractAnnotationResNameParameter(element, annotationName, DEFAULT_FIELD_NAME_RESNAME);
    }

    /**
     * extractAnnotationResNameParameter
     *
     * @param element element
     * @param annotationName annotationName
     * @param fieldName fieldName
     * @return resNames
     */
    public String[] extractAnnotationResNameParameter(Element element, String annotationName, String fieldName) {
        /*
         * Annotation resName() parameter can be a String or a String[]
         */
        Object annotationResName = extractAnnotationParameter(element, annotationName, fieldName);
        if (annotationResName == null) {
            // This case happened during refactoring, if the id has been changed
            // in the layout and compiler throws an error on the annotation
            // because the constant doesn't exists anymore
            return new String[0];
        }

        String[] resNames;
        if (annotationResName.getClass().isArray()) {
            resNames = (String[]) annotationResName;
        } else {
            resNames = new String[1];
            resNames[0] = (String) annotationResName;
        }
        return resNames;
    }

    /**
     * extractAnnotationResIdValueParameter
     *
     * @param element element
     * @param annotationName annotationName
     * @return extractAnnotationResIdValueParameter
     */
    public int[] extractAnnotationResIdValueParameter(Element element, String annotationName) {
        return extractAnnotationResIdValueParameter(element, annotationName, DEFAULT_FIELD_NAME_VALUE);
    }

    /**
     * extractAnnotationResIdValueParameter
     *
     * @param element element
     * @param annotationName annotationName
     * @param fieldName fieldName
     * @return values
     */
    public int[] extractAnnotationResIdValueParameter(Element element,
        String annotationName, String fieldName) {
        Object annotationValue = extractAnnotationParameter(element, annotationName, fieldName);
        if (annotationValue == null) {
            return new int[0];
        }

        int[] values;
        if (annotationValue.getClass().isArray()) {
            values = (int[]) annotationValue;
        } else {
            values = new int[1];
            values[0] = (Integer) annotationValue;
        }
        return values;
    }

    /**
     * extractAnnotationParameter
     *
     * @param element element
     * @param annotationName annotationName
     * @param methodName methodName
     * @param <T> T
     * @return RuntimeException
     */
    @SuppressWarnings("unchecked")
    public <T> T extractAnnotationParameter(Element element, String annotationName, String methodName) {
        Annotation annotation;
        try {
            annotation = element.getAnnotation((Class<? extends Annotation>) Class.forName(annotationName));
      } catch (ClassNotFoundException e) {
            e.printStackTrace();
            throw new RuntimeException("Could not load annotation class " + annotationName, e);
        }
        Method method;
        try {
            method = annotation.getClass().getMethod(methodName);
            return (T) method.invoke(annotation);
        } catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException e) {
            if (e.getCause() instanceof MirroredTypeException) {
                MirroredTypeException cause = (MirroredTypeException) e.getCause();
                return (T) cause.getTypeMirror();
            } else {
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * actionName
     *
     * @param annotationName annotationName
     * @return EditorAction
     */
    public String actionName(String annotationName) {
        if (OnAbilityResult.class.getName().equals(annotationName)) {
            return "Result";
        }

        if (EditorAction.class.getName().equals(annotationName)) {
            return EditorAction.class.getSimpleName();
        }
        String annotationSimpleName = annotationName.substring(annotationName.lastIndexOf('.') + 1);
        if (annotationSimpleName.endsWith("e")) {
            return annotationSimpleName + "d";
        }
        return annotationSimpleName + "ed";
    }

    /**
     * extractAnnotationClassArrayParameter
     *
     * @param element element
     * @param annotation annotation
     * @param methodName methodName
     * @return extractAnnotationClassArrayParameter
     */
    public List<DeclaredType> extractAnnotationClassArrayParameter(Element element,
        Class<? extends Annotation> annotation, String methodName) {
        return extractAnnotationClassArrayParameter(element, annotation.getName(), methodName);
    }

    /**
     * extractAnnotationClassArrayParameter
     *
     * @param element element
     * @param annotationName annotationName
     * @param methodName methodName
     * @return null
     */
    public List<DeclaredType> extractAnnotationClassArrayParameter(Element element,
        String annotationName, String methodName) {
        AnnotationMirror annotationMirror = findAnnotationMirror(element, annotationName);
        Map<? extends ExecutableElement, ? extends AnnotationValue> elementValues
            = annotationMirror.getElementValues();
        for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : elementValues.entrySet()) {
            if (methodName.equals(entry.getKey().getSimpleName().toString())) {
                AnnotationValue annotationValue = entry.getValue();
                @SuppressWarnings("unchecked")
                List<AnnotationValue> annotationClassArray = (List<AnnotationValue>) annotationValue.getValue();

                List<DeclaredType> result = new ArrayList<>(annotationClassArray.size());

                for (AnnotationValue annotationClassValue : annotationClassArray) {
                    result.add((DeclaredType) annotationClassValue.getValue());
                }
                return result;
            }
        }
        return null;
    }

    /**
     * extractAnnotationClassParameter
     *
     * @param element element
     * @param annotation annotation
     * @param methodName methodName
     * @return extractAnnotationClassParameter
     */
    public DeclaredType extractAnnotationClassParameter(Element element,
        Class<? extends Annotation> annotation, String methodName) {
        return extractAnnotationClassParameter(element, annotation.getName(), methodName);
    }

    /**
     * extractAnnotationClassParameter
     *
     * @param element element
     * @param annotationName annotationName
     * @param methodName methodName
     * @return null
     */
    public DeclaredType extractAnnotationClassParameter(Element element, String annotationName, String methodName) {
        AnnotationMirror annotationMirror = findAnnotationMirror(element, annotationName);

        Map<? extends ExecutableElement, ? extends AnnotationValue> elementValues
            = annotationMirror.getElementValues();

        for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : elementValues.entrySet()) {
            /*
             * "methodName" is unset when the default value is used
             */
            if (methodName.equals(entry.getKey().getSimpleName().toString())) {
                AnnotationValue annotationValue = entry.getValue();

                return (DeclaredType) annotationValue.getValue();
            }
        }

        return null;
    }

    /**
     * extractAnnotationClassParameter
     *
     * @param element element
     * @param annotationName annotationName
     * @return extractAnnotationClassParameter
     */
    public DeclaredType extractAnnotationClassParameter(Element element, String annotationName) {
        return extractAnnotationClassParameter(element, annotationName, DEFAULT_FIELD_NAME_VALUE);
    }

    /**
     * hasOneOfClassAnnotations
     *
     * @param element element
     * @param validAnnotation validAnnotation
     * @return hasOneOfClassAnnotations
     */
    public boolean hasOneOfClassAnnotations(Element element,
        Class<? extends Annotation> validAnnotation) {
        List<Class<? extends Annotation>> annotations = new ArrayList<>();
        annotations.add(validAnnotation);
        return hasOneOfClassAnnotations(element, annotations);
    }

    /**
     * enclosingElementHasEnhancedComponentAnnotation
     *
     * @param element element
     * @return hasOneOfClassAnnotations
     */
    public boolean enclosingElementHasEnhancedComponentAnnotation(Element element) {
        Element enclosingElement = element.getEnclosingElement();
        return hasOneOfClassAnnotations(enclosingElement, VALID_ENHANCED_COMPONENT_ANNOTATIONS);
    }

    /**
     * hasOneOfClassAnnotations
     *
     * @param element element
     * @param validAnnotations validAnnotations
     * @return false
     */
    public boolean hasOneOfClassAnnotations(Element element,
        List<Class<? extends Annotation>> validAnnotations) {
        for (Class<? extends Annotation> validAnnotation : validAnnotations) {
            if (element.getAnnotation(validAnnotation) != null) {
                return true;
            }
        }
        return false;
    }
}
